﻿using System;
using System.Collections.Generic;
using System.Linq;
using Xenta.Api;
using Xenta.Entities;
using Xenta.Enums;
using Xenta.Security;
using Xenta.Structs;
using Xenta.Utils;

namespace Xenta.Operations
{
    #region __General__

    /// <summary>
    /// Creates a stock entry.
    /// </summary>
    public class CreateStockEntry : CreateAuditable<StockEntryEntity>
    {
        #region Ctors

        /// <summary>
        /// Initializes a new class instance.
        /// </summary>
        public CreateStockEntry()
        {
            AdjustmentMin = 1;
            AdjustmentMax = 1000;
            ExhaustionThreshold = 10;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the item identifier.
        /// </summary>
        public int ItemID
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the minimum adjustment quantity.
        /// </summary>
        public decimal AdjustmentMin
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the maximum adjustment quantity.
        /// </summary>
        public decimal AdjustmentMax
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the reserve quantity.
        /// </summary>
        public decimal Reserve
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the exhaustion threshold.
        /// </summary>
        public decimal ExhaustionThreshold
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the flags.
        /// </summary>
        public StockEntryFlags Flags
        {
            get;
            set;
        }

        #endregion

        #region Utilities

        protected override Err Validate(IUnitOfWork uow)
        {
            if(AdjustmentMin <= Decimal.Zero)
                return Err.Of<StockEntryEntity>().NotValid("AdjustmentMin");
            if(AdjustmentMax <= Decimal.Zero)
                return Err.Of<StockEntryEntity>().NotValid("AdjustmentMax");
            return base.Validate(uow);
        }

        protected override void Construct(IUnitOfWork uow, StockEntryEntity entity)
        {
            base.Construct(uow, entity);

            entity.Item = uow.Execute(new GetSalesItem
            {
                ThrowsException = true,
                EntityID = ItemID
            }).Result;
            entity.AdjustmentMin = AdjustmentMin;
            entity.AdjustmentMax = AdjustmentMax;
            entity.Reserve = Reserve;
            entity.ExhaustionThreshold = ExhaustionThreshold;
            entity.Flags = Flags;
        }

        #endregion
    }

    /// <summary>
    /// Creates a stock entry.
    /// </summary>
    public class UpdateStockEntry : UpdateAuditable<StockEntryEntity>
    {
        #region Ctors

        /// <summary>
        /// Initializes a new class instance.
        /// </summary>
        public UpdateStockEntry()
        {
            AdjustmentMin = 1;
            AdjustmentMax = 1000;
            ExhaustionThreshold = 10;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the minimum adjustment quantity.
        /// </summary>
        public decimal AdjustmentMin
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the maximum adjustment quantity.
        /// </summary>
        public decimal AdjustmentMax
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the reserve quantity.
        /// </summary>
        public decimal Reserve
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the exhaustion threshold.
        /// </summary>
        public decimal ExhaustionThreshold
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the flags.
        /// </summary>
        public StockEntryFlags Flags
        {
            get;
            set;
        }

        #endregion

        #region Utilities

        protected override Err Validate(IUnitOfWork uow)
        {
            if(AdjustmentMin <= Decimal.Zero)
                return Err.Of<StockEntryEntity>().NotValid("AdjustmentMin");
            if(AdjustmentMax <= Decimal.Zero)
                return Err.Of<StockEntryEntity>().NotValid("AdjustmentMax");
            return base.Validate(uow);
        }

        protected override void Revise(IUnitOfWork uow, StockEntryEntity entity)
        {
            base.Revise(uow, entity);

            entity.AdjustmentMin = AdjustmentMin;
            entity.AdjustmentMax = AdjustmentMax;
            entity.Reserve = Reserve;
            entity.ExhaustionThreshold = ExhaustionThreshold;
            entity.Flags = Flags;
        }

        #endregion
    }

    /// <summary>
    /// Sets a stock entry.
    /// </summary>
    public class SetStockEntry : Operation<StockEntryEntity>,
        ISecuredOperation, IApiOperation
    {
        #region Ctors

        /// <summary>
        /// Initializes a new class instance.
        /// </summary>
        public SetStockEntry()
        {
            AdjustmentMin = 1;
            AdjustmentMax = 1000;
            ExhaustionThreshold = 10;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets the collection of security policies.
        /// </summary>
        public IEnumerable<SecurityPolicy> SecurityPolicies
        {
            get
            {
                yield return new SecurityPolicy()
                    .IsAuthenticated()
                    .HasPermission("SALES");
                yield return new SecurityPolicy()
                    .IsAuthenticated()
                    .IsVerified()
                    .Rule((ctx, uow) =>
                    {
                        var seller = ctx.Seller(uow);
                        if(seller == null || !seller.Flags.HasFlag(SellerFlags.Active))
                            return false;
                        var item = uow.Execute(new GetSalesItem
                        {
                            EntityID = ItemID
                        }).Result;
                        if(item == null)
                            return false;
                        return item.Seller.Equals(seller);
                    });
            }
        }

        /// <summary>
        /// Gets or sets the item identifier.
        /// </summary>
        public int ItemID
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the minimum adjustment quantity.
        /// </summary>
        public decimal AdjustmentMin
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the maximum adjustment quantity.
        /// </summary>
        public decimal AdjustmentMax
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the reserve quantity.
        /// </summary>
        public decimal Reserve
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the exhaustion threshold.
        /// </summary>
        public decimal ExhaustionThreshold
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the flags.
        /// </summary>
        public StockEntryFlags Flags
        {
            get;
            set;
        }

        #endregion

        #region Utilities

        protected override void Work(IUnitOfWork uow, out StockEntryEntity result)
        {
            var entity = uow.Execute(new GetStockEntryByItem
            {
                ItemID = ItemID
            }).Result;
            if(entity == null)
            {
                entity = uow.Execute(new CreateStockEntry
                {
                    ItemID = ItemID,
                    AdjustmentMin = AdjustmentMin,
                    AdjustmentMax = AdjustmentMax,
                    ExhaustionThreshold = ExhaustionThreshold,
                    Reserve = Reserve,
                    Flags = Flags
                }).Result;
            }
            else
            {
                entity = uow.Execute(new UpdateStockEntry
                {
                    EntityID = entity.EntityID,
                    AdjustmentMin = AdjustmentMin,
                    AdjustmentMax = AdjustmentMax,
                    ExhaustionThreshold = ExhaustionThreshold,
                    Reserve = Reserve,
                    Flags = Flags
                }).Result;
            }
            result = entity;
        }

        #endregion
    }

    /// <summary>
    /// Deletes the stock entry.
    /// </summary>
    public class DeleteStockEntry : DeleteEntity<StockEntryEntity>,
        ISecuredOperation, IApiOperation
    {
        #region Properties

        /// <summary>
        /// Gets the collection of security policies.
        /// </summary>
        public IEnumerable<SecurityPolicy> SecurityPolicies
        {
            get
            {
                yield return new SecurityPolicy()
                    .IsAuthenticated()
                    .HasPermission("SALES");
                yield return new SecurityPolicy()
                    .IsAuthenticated()
                    .IsVerified()
                    .Rule((ctx, uow) =>
                    {
                        var seller = ctx.Seller(uow);
                        if(seller == null || !seller.Flags.HasFlag(SellerFlags.Active))
                            return false;
                        var entry = uow.Execute(new GetStockEntry
                        {
                            EntityID = EntityID
                        }).Result;
                        if(entry == null)
                            return false;
                        return entry.Item.Seller.Equals(seller);
                    });
            }
        }

        #endregion
    }

    /// <summary>
    /// Gets the stock entry by identifier.
    /// </summary>
    public class GetStockEntry : GetEntity<StockEntryEntity>
    {
    }

    /// <summary>
    /// Gets the stock entry by sales item.
    /// </summary>
    public class GetStockEntryByItem : QueryEntity<StockEntryEntity>
    {
        #region Properties

        /// <summary>
        /// Gets or sets the item identifier.
        /// </summary>
        public int ItemID
        {
            get;
            set;
        }

        #endregion

        #region Utilities

        protected override IQueryable<StockEntryEntity> Build(IUnitOfWork uow, IQueryable<StockEntryEntity> query)
        {
            return base.Build(uow, query).Where(x => x.Item.EntityID == ItemID);
        }

        #endregion
    }

    /// <summary>
    /// Gets the stock entry by stock keeping unit.
    /// </summary>
    public class GetStockEntryBySKU : QueryEntity<StockEntryEntity>
    {
        #region Properties

        /// <summary>
        /// Gets or sets the stock keeping unit.
        /// </summary>
        public string SKU
        {
            get;
            set;
        }

        #endregion

        #region Utilities

        protected override IQueryable<StockEntryEntity> Build(IUnitOfWork uow, IQueryable<StockEntryEntity> query)
        {
            string sku = StringHelper.EnsureNotNull(SKU).TrimToLen(100);
            return base.Build(uow, query).Where(x => x.Item.SKU.Equals(sku));
        }

        #endregion
    }

    #endregion

    #region __Search__

    /// <summary>
    /// Search over stock entry collection.
    /// </summary>
    public class SearchStockEntries : SearchOperation<StockEntryEntity>,
        IApiOperation
    {
        #region Ctors

        /// <summary>
        /// Initializes a new class instance.
        /// </summary>
        public SearchStockEntries()
        {
            Term = SearchTerm.Empty;
            SortDir = SortDirection.Ascending;
            SortBy = new []
            {
                StockEntrySortBy.ID
            };
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the search term.
        /// </summary>
        public SearchTerm Term
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the flag mask.
        /// </summary>
        public StockEntryFlags FlagMask
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the sort direction.
        /// </summary>
        public SortDirection SortDir
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the sort by.
        /// </summary>
        public StockEntrySortBy[] SortBy
        {
            get;
            set;
        }

        #endregion

        #region Utilities

        protected override IQueryable<StockEntryEntity> Build(IUnitOfWork uow, IQueryable<StockEntryEntity> query)
        {
            query = base.Build(uow, query);
            query = query.FilterByFlags(x => (int)x.Flags, (int)FlagMask);
            switch(Term.Mode)
            {
                case SearchTerm.SearchMode.StartsWith:
                    query = query.Where(x => x.Item.SKU.StartsWith(Term.Keyword));
                    break;
                case SearchTerm.SearchMode.EndsWith:
                    query = query.Where(x => x.Item.SKU.EndsWith(Term.Keyword));
                    break;
                case SearchTerm.SearchMode.Contains:
                    query = query.Where(x => x.Item.SKU.Contains(Term.Keyword));
                    break;
                case SearchTerm.SearchMode.Equals:
                    query = query.Where(x => x.Item.SKU.Equals(Term.Keyword));
                    break;
            }
            if(SortBy != null)
            {
                for(int i = 0; i < SortBy.Length; i++)
                {
                    switch(SortBy[i])
                    {
                        case StockEntrySortBy.SKU:
                            query = query.OrderBy(x => x.Item.SKU, SortDir, i != 0);
                            break;
                        case StockEntrySortBy.Reserve:
                            query = query.OrderBy(x => x.Reserve, SortDir, i != 0);
                            break;
                        case StockEntrySortBy.CreatedOn:
                            query = query.OrderBy(x => x.CreatedOn, SortDir, i != 0);
                            break;
                        default:
                            query = query.OrderBy(x => x.EntityID, SortDir, i != 0);
                            break;
                    }
                }
            }
            return query;
        }

        #endregion
    }

    #endregion

    #region __Workflow__

    /// <summary>
    /// Checks if the required quantity is valid and available.
    /// </summary>
    public class CheckStockReserve : Operation<Boolean>
    {
        #region Properties

        /// <summary>
        /// Gets or sets the stock keeping unit.
        /// </summary>
        public string SKU
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the required quantity.
        /// </summary>
        public decimal Quantity
        {
            get;
            set;
        }

        #endregion

        #region Utilities

        protected override void Work(IUnitOfWork uow, out bool result)
        {
            result = true;

            if(Quantity <= Decimal.Zero)
                return;
            var entry = uow.Execute(new GetStockEntryBySKU
            {
                SKU = SKU
            }).Result;
            if(entry == null || !entry.Flags.HasFlag(StockEntryFlags.Active))
                return;

            if(Quantity < entry.AdjustmentMin || Quantity > entry.AdjustmentMax)
                result = false;
            if(Quantity > entry.Reserve && !entry.Flags.HasFlag(StockEntryFlags.OversoldAllowed))
                result = false;
        }

        #endregion
    }

    /// <summary>
    /// Adjusts stock reserve.
    /// </summary>
    public class AdjustStockReserve : Operation
    {
        #region Properties

        /// <summary>
        /// Gets or sets the stock keeping unit.
        /// </summary>
        public string SKU
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the quantity (positive or negative) to adjust the stock.
        /// </summary>
        public decimal Quantity
        {
            get;
            set;
        }

        #endregion

        #region Utilities

        protected override Err Validate(IUnitOfWork uow)
        {
            if(String.IsNullOrWhiteSpace(SKU))
                return Err.Of(this).NotValid(SKU);
            if(Quantity < Decimal.Zero && !uow.Execute(new CheckStockReserve
            {
                Quantity = Math.Abs(Quantity),
                SKU = SKU
            }).Result)
            {
                return Err.Of(this).NotPossible();
            }
            return base.Validate(uow);
        }

        protected override void Work(IUnitOfWork uow)
        {
            var entry = uow.Execute(new GetStockEntryBySKU
            {
                SKU = SKU
            }).Result;
            if(entry == null || !entry.Flags.HasFlag(StockEntryFlags.Active))
                return;

            decimal oldReserve = entry.Reserve;
            entry = uow.Execute(new SetStockEntry
            {
                ItemID = entry.Item.EntityID,
                AdjustmentMin = entry.AdjustmentMin,
                AdjustmentMax = entry.AdjustmentMax,
                Reserve = entry.Reserve + Quantity,
                ExhaustionThreshold = entry.ExhaustionThreshold,
                Flags = entry.Flags
            }).Result;

            if(!entry.Flags.HasFlag(StockEntryFlags.ExhaustionNotificationEnabled))
                return;
            if(oldReserve <= entry.ExhaustionThreshold || entry.Reserve > entry.ExhaustionThreshold)
                return;
            try
            {
                uow.Execute(new EnqueueStockExhaustionNotification
                {
                    EntryID = entry.EntityID,
                    RecipientID = entry.Item.Seller.Account.EntityID
                });
            }
            catch(Exception ex)
            {
                uow.Execute(new CreateLogRecordFromException
                {
                    Ex = ex
                });
            }
        }

        #endregion
    }

    #endregion

    #region __Notification__

    /// <summary>
    /// Enqueues a stock exhaustion notification message.
    /// </summary>
    /// <remarks>
    /// Message template code: WSTCKXHST
    /// Allowed tokens: [T:Recipient.EntityID], [T:Recipient.Guid], [T:Recipient.Email], [T:Recipient.Username],
    /// [T:StockEntry.Item.EntityID], [T:StockEntry.Item.SKU], [T:StockEntry.EntityID], [T:StockEntry.ExhaustionThreshold],
    /// [T:StockEntry.Reserve]
    /// </remarks>
    public class EnqueueStockExhaustionNotification : Operation<MessageEntity>
    {
        #region Properties

        /// <summary>
        /// Gets or sets the stock entry identifier.
        /// </summary>
        public int EntryID
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the recipient account identifier.
        /// </summary>
        public int RecipientID
        {
            get;
            set;
        }

        #endregion

        #region Utilities

        protected override void Work(IUnitOfWork uow, out MessageEntity result)
        {
            var entry = uow.Execute(new GetStockEntry
            {
                ThrowsException = true,
                EntityID = EntryID
            }).Result;
            var recipient = uow.Execute(new GetAccount
            {
                ThrowsException = true,
                EntityID = RecipientID
            }).Result;
            var usrLocale = recipient.LocalizationProfile;
            var numFmt = usrLocale.Country.NumericFormat;
            string exhaustionThresholdStr = numFmt.Convert(entry.ExhaustionThreshold);
            string reserveStr = numFmt.Convert(entry.Reserve);
            var item = entry.Item;

            result = uow.Execute(new CreateTemplatedMessageByCode
            {
                TemplateCode = "WSTCKXHST",
                LanguageID = usrLocale.Language.EntityID,
                Replacements = new Dictionary<String, String>
                {
                    {"Recipient.EntityID", recipient.EntityID.ToString()},
                    {"Recipient.Email", recipient.Email},
                    {"Recipient.Username", recipient.Username},
                    {"Recipient.Guid", recipient.Guid.ToString()},
                    {"StockEntry.Item.EntityID", item.EntityID.ToString()},
                    {"StockEntry.Item.SKU", item.SKU},
                    {"StockEntry.EntityID", entry.EntityID.ToString()},
                    {"StockEntry.ExhaustionThreshold", exhaustionThresholdStr},
                    {"StockEntry.Reserve", reserveStr}
                },
                To = recipient.Email,
                Flags = MessageFlags.Active
            }).Result;
        }

        #endregion
    }

    #endregion
}
